package com.mini.mybatis.mybatisministep07.datasource.pooled;

import com.mini.mybatis.mybatisministep07.datasource.unpooled.UnPooledDataSource;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.*;
import java.util.logging.Logger;

public class PooledDataSource implements DataSource {


    private org.slf4j.Logger logger = LoggerFactory.getLogger(PooledDataSource.class);

    // 池状态
    private final PoolState poolState = new PoolState(this);

    // 无池化数据源 - 组合
    private UnPooledDataSource dataSource;

    // 活跃连接数 - 即正在被使用的链接 - 忙碌状态
    protected int poolMaximumActiveConnections = 10;

    // 空闲连接数 - 即随时可以被取走使用的链接 - 就绪状态
    protected int poolMaximumIdleConnections = 5;

    // 在被强制返回之前,池中连接被检查的时间
    protected int poolMaximumCheckoutTime = 20000;

    // 这是给连接池一个打印日志状态机会的低层次设置,还有重新尝试获得连接, 这些情况下往往需要很长时间 为了避免连接池没有配置时静默失败)。
    protected int poolTimeToWait = 20000;

    // 发送到数据的侦测查询,用来验证连接是否正常工作,并且准备 接受请求。默认是“NO PING QUERY SET” ,这会引起许多数据库驱动连接由一 个错误信息而导致失败
    protected String poolPingQuery = "NO PING QUERY SET";

    // 开启或禁用侦测查询
    protected boolean poolPingEnabled = false;

    // 用来配置 poolPingQuery 多次时间被用一次
    protected int poolPingConnectionsNotUsedFor = 0;

    private int expectedConnectionTypeCode;

    public PooledDataSource() {
        this.dataSource = new UnPooledDataSource();
    }


    protected void pushConnection(PooledConnection conn) throws SQLException {
        synchronized (poolState){
            poolState.activeConnections.remove(conn);
            if (conn.isValid()) {
                // 如果空闲连接数没有达到最大空闲连接数，将当前链接放到空闲连接列表
                if (poolState.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
                    poolState.accumulatedCheckoutTime += conn.getCheckoutTime();
                    if (!conn.getRealConnection().getAutoCommit()) {
                        conn.getRealConnection().rollback();
                    }
                    // 实例化一个新的代理链接，放到空闲列表中
                    PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
                    newConn.setCheckoutTimestamp(System.currentTimeMillis());
                    newConn.setLastUsedTimestamp(System.currentTimeMillis());
                    conn.invalid();
                    logger.info("Returned connection " + newConn.getRealHashCode() + " to pool.");

                    // 通知唤醒其它线程获取当前链接
                    poolState.notifyAll();

                } else {
                    // 空闲连接列表已满，放弃并关闭当前链接
                    poolState.accumulatedCheckoutTime += conn.getCheckoutTime();
                    if (!conn.getRealConnection().getAutoCommit()) {
                        conn.getRealConnection().rollback();
                    }
                    // 将connection关闭
                    conn.getRealConnection().close();
                    logger.info("Closed connection " + conn.getRealHashCode() + ".");
                    conn.invalid();
                }
            }else {
                logger.info("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
                poolState.badConnectionCount++;
            }
        }
    }


    protected PooledConnection popConnection(String username, String password) throws SQLException {

        boolean countedWait = false;
        PooledConnection conn = null;
        long t = System.currentTimeMillis();
        int localBadConnectionCount = 0;

        while (conn == null){
            synchronized (poolState) {
                // 如果有空闲连接，则直接返回空闲列表的第一个链接
                if (!poolState.idleConnections.isEmpty()) {
                    conn = poolState.idleConnections.remove(0);
                    logger.info("Check out connection " + conn.getRealHashCode() + " from pool...");
                } else {
                    // 如果无空闲连接
                    if (poolState.activeConnections.size() < poolMaximumActiveConnections) {
                        // 如果活跃链接数小于最大活跃连接数,则创建新的链接
                        conn = new PooledConnection(dataSource.getConnection(), this);
                        logger.info("Created connection " + conn.getRealHashCode() + ".");
                    } else {
                        // 已达到最大活跃连接数，取最早活跃的（最老的）链接
                        PooledConnection oldestActiveConnection = poolState.activeConnections.get(0);
                        // 拿到最老链接的最后校验时间
                        long oldestConnectionCheckoutTime = oldestActiveConnection.getCheckoutTime();
                        if (oldestConnectionCheckoutTime > poolMaximumCheckoutTime) {
                            // 已经超过了最大校验时间，此链接标志为过期
                            poolState.claimedOverdueConnectionCount++;
                            poolState.accumulatedCheckoutTimeOfOverdueConnections += oldestConnectionCheckoutTime;
                            poolState.accumulatedCheckoutTime += oldestConnectionCheckoutTime;
                            poolState.activeConnections.remove(oldestActiveConnection);
                            // 如果当前链接未设置自动提交，则直接回滚
                            if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                                oldestActiveConnection.getRealConnection().rollback();
                            }
                            // 拿到这个最老的链接的真实链接，创建新的代理链接
                            conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                            oldestActiveConnection.invalid();
                            logger.info("Claimed overdue connection " + conn.getRealHashCode() + ".");
                        } else {
                            try {
                                // 如果没超过最大checkout时间，则只能阻塞等待唤醒，从而继续循环获取链接
                                if (!countedWait) {
                                    poolState.hadToWaitCount++;
                                    countedWait = true;
                                }
                                logger.info("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                                long wt = System.currentTimeMillis();
                                // 当前获取链接线程释放锁，等待新的链接后被唤醒。继续循环尝试获取链接
                                poolState.wait(poolTimeToWait);
                                poolState.accumulatedWaitTime += System.currentTimeMillis() - wt;
                            } catch (InterruptedException e) {
                                break;
                            }
                        }
                    }
                }

                // 获取到新的链接
                if (conn != null) {
                    if (conn.isValid()) {
                        if (!conn.getRealConnection().getAutoCommit()) {
                            conn.getRealConnection().rollback();
                        }
                        conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
                        // 记录checkout时间
                        conn.setCheckoutTimestamp(System.currentTimeMillis());
                        conn.setLastUsedTimestamp(System.currentTimeMillis());
                        // 将新建立的链接放入活跃链接列表
                        poolState.activeConnections.add(conn);
                        poolState.requestCount++;
                        poolState.accumulatedRequestTime += System.currentTimeMillis() - t;
                    } else {
                        logger.info("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                        // 如果没拿到，统计信息：失败链接 +1
                        poolState.badConnectionCount++;
                        localBadConnectionCount++;
                        conn = null;
                        // 失败次数较多，抛异常
                        if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
                            logger.debug("PooledDataSource: Could not get a good connection to the database.");
                            throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                        }
                    }
                }
            }
        }

        if (conn == null) {
            logger.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
            throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
        }

        return conn;
    }

    protected void forceCloseAll(){
        synchronized (poolState) {
            expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
            // 关闭活跃链接
            for (int i = poolState.activeConnections.size(); i > 0; i--) {
                try {
                    PooledConnection conn = poolState.activeConnections.remove(i - 1);
                    conn.invalid();

                    Connection realConn = conn.getRealConnection();
                    if (!realConn.getAutoCommit()) {
                        realConn.rollback();
                    }
                    realConn.close();
                } catch (Exception ignore) {

                }
            }
            // 关闭空闲链接
            for (int i = poolState.idleConnections.size(); i > 0; i--) {
                try {
                    PooledConnection conn = poolState.idleConnections.remove(i - 1);
                    conn.invalid();

                    Connection realConn = conn.getRealConnection();
                    if (!realConn.getAutoCommit()) {
                        realConn.rollback();
                    }
                } catch (Exception ignore) {

                }
            }
            logger.info("PooledDataSource forcefully closed/removed all connections.");
        }
    }

    // 测试当前链接是否有效
    protected boolean pingConnection(PooledConnection conn){
        boolean result = false;

        try {
            result = !conn.getRealConnection().isClosed();
        } catch (SQLException e) {
            logger.info("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
            result = false;
        }

        if (result){
            if (poolPingEnabled){
                if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
                    try {
                        logger.info("Testing connection " + conn.getRealHashCode() + " ...");
                        Connection realConn = conn.getRealConnection();
                        Statement statement = realConn.createStatement();
                        ResultSet resultSet = statement.executeQuery(poolPingQuery);
                        resultSet.close();
                        if (!realConn.getAutoCommit()) {
                            realConn.rollback();
                        }
                        result = true;
                        logger.info("Connection " + conn.getRealHashCode() + " is GOOD!");
                    } catch (Exception e) {
                        logger.info("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
                        try {
                            conn.getRealConnection().close();
                        } catch (SQLException ignore) {
                        }
                        result = false;
                        logger.info("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
                    }
                }
            }
        }
        return result;

    }

    /**
     * 获取真实的数据库连接
     * @param conn
     * @return
     */
    public static Connection unwrapConnection(Connection conn){
        if (Proxy.isProxyClass(conn.getClass())){
            InvocationHandler handler = Proxy.getInvocationHandler(conn);
            if (handler instanceof PooledConnection){
                return ((PooledConnection) handler).getRealConnection();
            }
        }
        return conn;
    }


    private int assembleConnectionTypeCode(String url, String username, String password) {
        return ("" + url + username + password).hashCode();
    }

    @Override
    public Connection getConnection() throws SQLException {
        return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return popConnection(username,password).getProxyConnection();
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        throw new SQLException(getClass().getName() + " is not a wrapper.");
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return DriverManager.getLogWriter();
    }

    @Override
    public void setLogWriter(PrintWriter logWriter) throws SQLException {
        DriverManager.setLogWriter(logWriter);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }


    public String getDriver() {
        return this.dataSource.getDriver();
    }

    public synchronized void setDriver(String driver) {
        this.dataSource.setDriver(driver);
        forceCloseAll();
    }

    public String getUrl() {
        return this.dataSource.getUrl();
    }

    public void setUrl(String url) {
        this.dataSource.setUrl(url);
        forceCloseAll();
    }

    public String getUsername() {
        return this.dataSource.getUsername();
    }

    public void setUsername(String username) {
        this.dataSource.setUsername(username);
        forceCloseAll();
    }

    public String getPassword() {
        return this.dataSource.getPassword();
    }

    public void setPassword(String password) {
        this.dataSource.setPassword(password);
        forceCloseAll();
    }

}
